/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

using System;
using java = biz.ritter.javapi;

namespace org.apache.commons.compress.archivers.tar{


    /**
     * This class represents an entry in a Tar archive. It consists
     * of the entry's header, as well as the entry's File. Entries
     * can be instantiated in one of three ways, depending on how
     * they are to be used.
     * <p>
     * TarEntries that are created from the header bytes read from
     * an archive are instantiated with the TarEntry( byte[] )
     * constructor. These entries will be used when extracting from
     * or listing the contents of an archive. These entries have their
     * header filled in using the header bytes. They also set the File
     * to null, since they reference an archive entry not a file.
     * <p>
     * TarEntries that are created from Files that are to be written
     * into an archive are instantiated with the TarEntry( File )
     * constructor. These entries have their header filled in using
     * the File's information. They also keep a reference to the File
     * for convenience when writing entries.
     * <p>
     * Finally, TarEntries can be constructed from nothing but a name.
     * This allows the programmer to construct the entry by hand, for
     * instance when only an InputStream is available for writing to
     * the archive, and the header information is constructed from
     * other information. In this case the header fields are set to
     * defaults and the File is set to null.
     *
     * <p>
     * The C structure for a Tar Entry's header is:
     * <pre>
     * struct header {
     * char name[100];     // TarConstants.NAMELEN    - offset   0
     * char mode[8];       // TarConstants.MODELEN    - offset 100
     * char uid[8];        // TarConstants.UIDLEN     - offset 108
     * char gid[8];        // TarConstants.GIDLEN     - offset 116
     * char size[12];      // TarConstants.SIZELEN    - offset 124
     * char mtime[12];     // TarConstants.MODTIMELEN - offset 136
     * char chksum[8];     // TarConstants.CHKSUMLEN  - offset 148
     * char linkflag[1];   //                         - offset 156
     * char linkname[100]; // TarConstants.NAMELEN    - offset 157
     * The following fields are only present in new-style POSIX tar archives:
     * char magic[6];      // TarConstants.MAGICLEN   - offset 257
     * char version[2];    // TarConstants.VERSIONLEN - offset 263
     * char uname[32];     // TarConstants.UNAMELEN   - offset 265
     * char gname[32];     // TarConstants.GNAMELEN   - offset 297
     * char devmajor[8];   // TarConstants.DEVLEN     - offset 329
     * char devminor[8];   // TarConstants.DEVLEN     - offset 337
     * char prefix[155];   // TarConstants.PREFIXLEN  - offset 345
     * // Used if "name" field is not long enough to hold the path
     * char pad[12];       // NULs                    - offset 500
     * } header;
     * All unused bytes are set to null.
     * New-style GNU tar files are slightly different from the above.
     * </pre>
     * 
     * @NotThreadSafe
     */

    public class TarArchiveEntry : TarConstants, ArchiveEntry {
        /** The entry's name. */
        private String name;

        /** The entry's permission mode. */
        private int mode;

        /** The entry's user id. */
        private int userId;

        /** The entry's group id. */
        private int groupId;

        /** The entry's size. */
        private long size;

        /** The entry's modification time. */
        private long modTime;

        /** The entry's link flag. */
        private byte linkFlag;

        /** The entry's link name. */
        private String linkName;

        /** The entry's magic tag. */
        private String magic;
        /** The version of the format */
        private String version;

        /** The entry's user name. */
        private String userName;

        /** The entry's group name. */
        private String groupName;

        /** The entry's major device number. */
        private int devMajor;

        /** The entry's minor device number. */
        private int devMinor;

        /** The entry's file reference */
        private java.io.File file;

        /** Maximum length of a user's name in the tar file */
        public static readonly int MAX_NAMELEN = 31;

        /** Default permissions bits for directories */
        public static readonly int DEFAULT_DIR_MODE = 040755;

        /** Default permissions bits for files */
        public static readonly int DEFAULT_FILE_MODE = 0100644;

        /** Convert millis to seconds */
        public static readonly int MILLIS_PER_SECOND = 1000;

        /**
         * Construct an empty entry and prepares the header values.
         */
        private TarArchiveEntry () {
            init("", false);
        }

        /**
         * Construct an entry with only a name. This allows the programmer
         * to construct the entry's header "by hand". File is set to null.
         *
         * @param name the entry name
         */
        public TarArchiveEntry(String name) {
            init(name, false);
        }

        protected void init(String name, bool preserveLeadingSlashes)
        {
            this.magic = TarConstants.MAGIC_POSIX;
            this.version = TarConstants.VERSION_POSIX;
            this.name = "";
            this.linkName = "";

            String user = java.lang.SystemJ.getProperty("user.name", "");

            if (user.length() > MAX_NAMELEN)
            {
                user = user.Substring(0, MAX_NAMELEN);
            }

            this.userId = 0;
            this.groupId = 0;
            this.userName = user;
            this.groupName = "";
            this.file = null;

        }

        /**
         * Construct an entry with only a name. This allows the programmer
         * to construct the entry's header "by hand". File is set to null.
         *
         * @param name the entry name
         * @param preserveLeadingSlashes whether to allow leading slashes
         * in the name.
         * 
         * @since Apache Commons Compress 1.1
         */
        public TarArchiveEntry(String name, bool preserveLeadingSlashes) {
            init(name, preserveLeadingSlashes);

            name = normalizeFileName(name, preserveLeadingSlashes);
            bool isDir = name.EndsWith("/");

            this.devMajor = 0;
            this.devMinor = 0;
            this.name = name;
            this.mode = isDir ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE;
            this.linkFlag = isDir ? TarConstants.LF_DIR : TarConstants.LF_NORMAL;
            this.userId = 0;
            this.groupId = 0;
            this.size = 0;
            this.modTime = (new java.util.Date()).getTime() / MILLIS_PER_SECOND;
            this.linkName = "";
            this.userName = "";
            this.groupName = "";
            this.devMajor = 0;
            this.devMinor = 0;

        }

        /**
         * Construct an entry with a name and a link flag.
         *
         * @param name the entry name
         * @param linkFlag the entry link flag.
         */
        public TarArchiveEntry(String name, byte linkFlag) {
            init(name,false);
            this.linkFlag = linkFlag;
            if (linkFlag == TarConstants.LF_GNUTYPE_LONGNAME)
            {
                magic = TarConstants.MAGIC_GNU;
                version = TarConstants.VERSION_GNU_SPACE;
            }
        }

        /**
         * Construct an entry for a file. File is set to file, and the
         * header is constructed from information from the file.
         * The name is set from the normalized file path.
         *
         * @param file The file that the entry represents.
         */
        public TarArchiveEntry(java.io.File file) {
            initOverFile(file, normalizeFileName(file.getPath(), false));
        }

        protected void initOverFile (java.io.File file, String fileName) {
            this.file = file;

            this.linkName = "";

            if (file.isDirectory())
            {
                this.mode = DEFAULT_DIR_MODE;
                this.linkFlag = TarConstants.LF_DIR;

                int nameLength = fileName.length();
                if (nameLength == 0 || fileName.charAt(nameLength - 1) != '/')
                {
                    this.name = fileName + "/";
                }
                else
                {
                    this.name = fileName;
                }
                this.size = 0;
            }
            else
            {
                this.mode = DEFAULT_FILE_MODE;
                this.linkFlag = TarConstants.LF_NORMAL;
                this.size = file.length();
                this.name = fileName;
            }

            this.modTime = file.lastModified() / MILLIS_PER_SECOND;
            this.devMajor = 0;
            this.devMinor = 0;
        }
    
        /**
         * Construct an entry for a file. File is set to file, and the
         * header is constructed from information from the file.
         *
         * @param file The file that the entry represents.
         * @param fileName the name to be used for the entry.
         */
        public TarArchiveEntry(java.io.File file, String fileName) {
            init("",false);
            initOverFile(file, fileName);
        }

        /**
         * Construct an entry from an archive's header bytes. File is set
         * to null.
         *
         * @param headerBuf The header bytes from a tar archive entry.
         */
        public TarArchiveEntry(byte[] headerBuf) {
            init("",false);
            parseTarHeader(headerBuf);
        }

        /**
         * Determine if the two entries are equal. Equality is determined
         * by the header names being equal.
         *
         * @param it Entry to be checked for equality.
         * @return True if the entries are equal.
         */
        public bool equals(TarArchiveEntry it) {
            return getName().equals(it.getName());
        }

        /**
         * Determine if the two entries are equal. Equality is determined
         * by the header names being equal.
         *
         * @param it Entry to be checked for equality.
         * @return True if the entries are equal.
         */
        public override bool Equals(Object it) {
            if (it == null || GetType() != it.GetType()) {
                return false;
            }
            return equals((TarArchiveEntry) it);
        }

        /**
         * Hashcodes are based on entry names.
         *
         * @return the entry hashcode
         */
        public override int GetHashCode() {
            return getName().hashcode();
        }

        /**
         * Determine if the given entry is a descendant of this entry.
         * Descendancy is determined by the name of the descendant
         * starting with this entry's name.
         *
         * @param desc Entry to be checked as a descendent of this.
         * @return True if entry is a descendant of this.
         */
        public bool isDescendent(TarArchiveEntry desc) {
            return desc.getName().StartsWith(getName());
        }

        /**
         * Get this entry's name.
         *
         * @return This entry's name.
         */
        public String getName() {
            return name.toString();
        }

        /**
         * Set this entry's name.
         *
         * @param name This entry's new name.
         */
        public void setName(String name) {
            this.name = normalizeFileName(name, false);
        }

        /**
         * Set the mode for this entry
         *
         * @param mode the mode for this entry
         */
        public void setMode(int mode) {
            this.mode = mode;
        }

        /**
         * Get this entry's link name.
         *
         * @return This entry's link name.
         */
        public String getLinkName() {
            return linkName.toString();
        }

        /**
         * Set this entry's link name.
         * 
         * @param link the link name to use.
         * 
         * @since Apache Commons Compress 1.1
         */
        public void setLinkName(String link) {
            this.linkName = link;
        }

        /**
         * Get this entry's user id.
         *
         * @return This entry's user id.
         */
        public int getUserId() {
            return userId;
        }

        /**
         * Set this entry's user id.
         *
         * @param userId This entry's new user id.
         */
        public void setUserId(int userId) {
            this.userId = userId;
        }

        /**
         * Get this entry's group id.
         *
         * @return This entry's group id.
         */
        public int getGroupId() {
            return groupId;
        }

        /**
         * Set this entry's group id.
         *
         * @param groupId This entry's new group id.
         */
        public void setGroupId(int groupId) {
            this.groupId = groupId;
        }

        /**
         * Get this entry's user name.
         *
         * @return This entry's user name.
         */
        public String getUserName() {
            return userName.toString();
        }

        /**
         * Set this entry's user name.
         *
         * @param userName This entry's new user name.
         */
        public void setUserName(String userName) {
            this.userName = userName;
        }

        /**
         * Get this entry's group name.
         *
         * @return This entry's group name.
         */
        public String getGroupName() {
            return groupName.toString();
        }

        /**
         * Set this entry's group name.
         *
         * @param groupName This entry's new group name.
         */
        public void setGroupName(String groupName) {
            this.groupName = groupName;
        }

        /**
         * Convenience method to set this entry's group and user ids.
         *
         * @param userId This entry's new user id.
         * @param groupId This entry's new group id.
         */
        public void setIds(int userId, int groupId) {
            setUserId(userId);
            setGroupId(groupId);
        }

        /**
         * Convenience method to set this entry's group and user names.
         *
         * @param userName This entry's new user name.
         * @param groupName This entry's new group name.
         */
        public void setNames(String userName, String groupName) {
            setUserName(userName);
            setGroupName(groupName);
        }

        /**
         * Set this entry's modification time. The parameter passed
         * to this method is in "Java time".
         *
         * @param time This entry's new modification time.
         */
        public void setModTime(long time) {
            modTime = time / MILLIS_PER_SECOND;
        }

        /**
         * Set this entry's modification time.
         *
         * @param time This entry's new modification time.
         */
        public void setModTime(java.util.Date time) {
            modTime = time.getTime() / MILLIS_PER_SECOND;
        }

        /**
         * Set this entry's modification time.
         *
         * @return time This entry's new modification time.
         */
        public java.util.Date getModTime() {
            return new java.util.Date(modTime * MILLIS_PER_SECOND);
        }

        /** {@inheritDoc} */
        public java.util.Date getLastModifiedDate() {
            return getModTime();
        }

        /**
         * Get this entry's file.
         *
         * @return This entry's file.
         */
        public java.io.File getFile() {
            return file;
        }

        /**
         * Get this entry's mode.
         *
         * @return This entry's mode.
         */
        public int getMode() {
            return mode;
        }

        /**
         * Get this entry's file size.
         *
         * @return This entry's file size.
         */
        public long getSize() {
            return size;
        }

        /**
         * Set this entry's file size.
         *
         * @param size This entry's new file size.
         * @throws IllegalArgumentException if the size is < 0
         * or > {@link TarConstants#MAXSIZE} (077777777777L).
         */
        public void setSize(long size) {
            if (size > TarConstants.MAXSIZE || size < 0)
            {
                throw new java.lang.IllegalArgumentException("Size is out of range: "+size);
            }
            this.size = size;
        }


        /**
         * Indicate if this entry is a GNU long name block
         *
         * @return true if this is a long name extension provided by GNU tar
         */
        public bool isGNULongNameEntry() {
            return linkFlag == TarConstants.LF_GNUTYPE_LONGNAME
                && name.toString().Equals(TarConstants.GNU_LONGLINK);
        }

        /**
         * Check if this is a Pax header.
         * 
         * @return <code>true</code> if this is a Pax header.
         * 
         * @since Apache Commons Compress 1.1
         */
        public bool isPaxHeader(){
            return linkFlag == TarConstants.LF_PAX_EXTENDED_HEADER_LC
                || linkFlag == TarConstants.LF_PAX_EXTENDED_HEADER_UC;
        }

        /**
         * Check if this is a Pax header.
         * 
         * @return <code>true</code> if this is a Pax header.
         * 
         * @since Apache Commons Compress 1.1
         */
        public bool isGlobalPaxHeader(){
            return linkFlag == TarConstants.LF_PAX_GLOBAL_EXTENDED_HEADER;
        }

        /**
         * Return whether or not this entry represents a directory.
         *
         * @return True if this entry is a directory.
         */
        public bool isDirectory() {
            if (file != null) {
                return file.isDirectory();
            }

            if (linkFlag == TarConstants.LF_DIR)
            {
                return true;
            }

            if (getName().EndsWith("/")) {
                return true;
            }

            return false;
        }

        /**
         * If this entry represents a file, and the file is a directory, return
         * an array of TarEntries for this entry's children.
         *
         * @return An array of TarEntry's for this entry's children.
         */
        public TarArchiveEntry[] getDirectoryEntries() {
            if (file == null || !file.isDirectory()) {
                return new TarArchiveEntry[0];
            }

            String[]   list = file.list();
            TarArchiveEntry[] result = new TarArchiveEntry[list.Length];

            for (int i = 0; i < list.Length; ++i) {
                result[i] = new TarArchiveEntry(new java.io.File(file, list[i]));
            }

            return result;
        }

        /**
         * Write an entry's header information to a header buffer.
         *
         * @param outbuf The tar entry header buffer to fill in.
         */
        public void writeEntryHeader(byte[] outbuf) {
            int offset = 0;

            offset = TarUtils.formatNameBytes(name, outbuf, offset, TarConstants.NAMELEN);
            offset = TarUtils.formatOctalBytes(mode, outbuf, offset, TarConstants.MODELEN);
            offset = TarUtils.formatOctalBytes(userId, outbuf, offset, TarConstants.UIDLEN);
            offset = TarUtils.formatOctalBytes(groupId, outbuf, offset, TarConstants.GIDLEN);
            offset = TarUtils.formatLongOctalBytes(size, outbuf, offset, TarConstants.SIZELEN);
            offset = TarUtils.formatLongOctalBytes(modTime, outbuf, offset, TarConstants.MODTIMELEN);

            int csOffset = offset;

            for (int c = 0; c < TarConstants.CHKSUMLEN; ++c)
            {
                outbuf[offset++] = (byte) ' ';
            }

            outbuf[offset++] = linkFlag;
            offset = TarUtils.formatNameBytes(linkName, outbuf, offset, TarConstants.NAMELEN);
            offset = TarUtils.formatNameBytes(magic, outbuf, offset, TarConstants.MAGICLEN);
            offset = TarUtils.formatNameBytes(version, outbuf, offset, TarConstants.VERSIONLEN);
            offset = TarUtils.formatNameBytes(userName, outbuf, offset, TarConstants.UNAMELEN);
            offset = TarUtils.formatNameBytes(groupName, outbuf, offset, TarConstants.GNAMELEN);
            offset = TarUtils.formatOctalBytes(devMajor, outbuf, offset, TarConstants.DEVLEN);
            offset = TarUtils.formatOctalBytes(devMinor, outbuf, offset, TarConstants.DEVLEN);

            while (offset < outbuf.Length) {
                outbuf[offset++] = 0;
            }

            long chk = TarUtils.computeCheckSum(outbuf);

            TarUtils.formatCheckSumOctalBytes(chk, outbuf, csOffset, TarConstants.CHKSUMLEN);
        }

        /**
         * Parse an entry's header information from a header buffer.
         *
         * @param header The tar entry header buffer to get information from.
         */
        public void parseTarHeader(byte[] header) {
            int offset = 0;

            name = TarUtils.parseName(header, offset, TarConstants.NAMELEN);
            offset += TarConstants.NAMELEN;
            mode = (int)TarUtils.parseOctal(header, offset, TarConstants.MODELEN);
            offset += TarConstants.MODELEN;
            userId = (int)TarUtils.parseOctal(header, offset, TarConstants.UIDLEN);
            offset += TarConstants.UIDLEN;
            groupId = (int)TarUtils.parseOctal(header, offset, TarConstants.GIDLEN);
            offset += TarConstants.GIDLEN;
            size = TarUtils.parseOctal(header, offset, TarConstants.SIZELEN);
            offset += TarConstants.SIZELEN;
            modTime = TarUtils.parseOctal(header, offset, TarConstants.MODTIMELEN);
            offset += TarConstants.MODTIMELEN;
            offset += TarConstants.CHKSUMLEN;
            linkFlag = header[offset++];
            linkName = TarUtils.parseName(header, offset, TarConstants.NAMELEN);
            offset += TarConstants.NAMELEN;
            magic = TarUtils.parseName(header, offset, TarConstants.MAGICLEN);
            offset += TarConstants.MAGICLEN;
            version = TarUtils.parseName(header, offset, TarConstants.VERSIONLEN);
            offset += TarConstants.VERSIONLEN;
            userName = TarUtils.parseName(header, offset, TarConstants.UNAMELEN);
            offset += TarConstants.UNAMELEN;
            groupName = TarUtils.parseName(header, offset, TarConstants.GNAMELEN);
            offset += TarConstants.GNAMELEN;
            devMajor = (int)TarUtils.parseOctal(header, offset, TarConstants.DEVLEN);
            offset += TarConstants.DEVLEN;
            devMinor = (int)TarUtils.parseOctal(header, offset, TarConstants.DEVLEN);
            offset += TarConstants.DEVLEN;
            String prefix = TarUtils.parseName(header, offset, TarConstants.PREFIXLEN);
            // SunOS tar -E does not add / to directory names, so fix up to be consistent
            if (isDirectory() && !name.EndsWith("/")){
                name = name + "/";
            }
            if (prefix.length() >0){
                name = prefix + "/" + name;
            }
        }

        /**
         * Strips Windows' drive letter as well as any leading slashes,
         * turns path separators into forward slahes.
         */
        private static String normalizeFileName(String fileName, bool preserveLeadingSlashes) {
            String osname = java.lang.SystemJ.getProperty("os.name").ToLower(new System.Globalization.CultureInfo("en"));

            if (osname != null) {

                // Strip off drive letters!
                // REVIEW Would a better check be "(File.separator == '\')"?

                if (osname.StartsWith("windows")) {
                    if (fileName.length() > 2) {
                        char ch1 = fileName.charAt(0);
                        char ch2 = fileName.charAt(1);

                        if (ch2 == ':'
                            && ((ch1 >= 'a' && ch1 <= 'z')
                                || (ch1 >= 'A' && ch1 <= 'Z'))) {
                            fileName = fileName.Substring(2);
                        }
                    }
                } else if (osname.IndexOf("netware") > -1) {
                    int colon = fileName.indexOf(':');
                    if (colon != -1) {
                        fileName = fileName.Substring(colon + 1);
                    }
                }
            }

            fileName = fileName.Replace(java.io.File.separatorChar, '/');

            // No absolute pathnames
            // Windows (and Posix?) paths can start with "\\NetworkDrive\",
            // so we loop on starting /'s.
            while (!preserveLeadingSlashes && fileName.StartsWith("/")) {
                fileName = fileName.Substring(1);
            }
            return fileName;
        }
    }

}